import logging
from .case import Case
from .test import Test
from .worker import select_worker
from .device import Controller, FakeDevice, DevDisk, StorageController
from abc import ABCMeta, abstractmethod
from .utils import *


class Component():
    __metaclass__ = ABCMeta

    def __init__(self, logdir, cases, category, args):
        self.devices = list()
        self.output = False if args.list_hardware else True
        self.logdir = logdir
        self.cases = list()
        self.test_device_index = args.index - 1
        self.category = category
        self.mode = args.mode
        self.workers = list()
        self.groups = list()
        self.available = list()
        self.unavailable = list()
        self.arch = get_architecture()
        self.name = self.__class__.__name__.lower()
        self.tested_controller = []
        self.is_virtual_platform = is_virtual_platform()
        self.env = {}
        self.is_single_mode = args.single_mode
        if self.is_single_mode:
            self.env.update({'ANCERT_SYSTEM_TEST_MODE':'single'})
        else:
            self.env.update({'ANCERT_SYSTEM_TEST_MODE':'lts'})
        if args.category == 'System' or args.list_hardware == 'System':
            self.is_system_test = True
        else:
            self.is_system_test = False


    @abstractmethod
    def build(self, dstree):
        pass

    @abstractmethod
    def build_works(self):
        pass

    def build_cases(self, cases):
        if not cases:
            return
        for case in cases['cases']:
            c = Case()
            setattr(c, 'component', self.name)
            for key, val in case.items():
                setattr(c, key, val)
            self.cases.append(c)
        for key, val in cases['testgroup'].items():
            cases_inst = []
            for c_str in val:
                for c_inst in self.cases:
                    if set(c_inst.required) & set(['x86', 'arm', 'loongarch']):
                        if self.arch not in c_inst.required:
                            logging.debug('%s dose not support %s platform'
                            % (c_inst, self.arch))
                            continue
                    if c_str == c_inst.name:
                        setattr(c_inst, 'group', key)
                        cases_inst.append(c_inst)
                        break
            if cases_inst:
                self.groups.append([key, cases_inst])

    def display(self, msg):
        if self.available and self.output:
            print('Available %ss:' % msg)
            for ctr in self.available:
                print('\t%s [%s]' % (ctr.devinfo.split('|')[0], ctr.driver))
        if self.unavailable and self.output:
            print('Unavailable %ss:' % msg)
            for ctr in self.unavailable:
                print('\t%s %s' % (ctr.devinfo.split('|')[0], '[%s]' % ctr.driver \
                      if ctr.driver else '[* No Driver *]'))
        logging.info('Available %ss:' % msg)
        for ctr in self.available:
            logging.info('%s [%s]' % (ctr.devinfo, ctr.driver))
        logging.info('Unavailable %ss:' % msg)
        for ctr in self.unavailable:
            logging.info('%s [%s]' % (ctr.devinfo, ctr.driver))

    def sort(self):
        self.available = sorted(self.available, key=lambda d: d.name)
        self.unavailable = sorted(self.unavailable, key=lambda d: d.name)

    def next_tasks(self):
        for workers in self.workers:
            yield workers

    def dump_device_info(self):
        info = {'tested': [],
                'available': [],
                'unavailable': []}
        for tested in self.tested_controller:
            tested.update()
            info['tested'].append({self.name: tested.properties})
        for ctr in self.available:
            ctr.update()
            info['available'].append({self.name: ctr.properties})
        for ctr in self.unavailable:
            ctr.update()
            info['unavailable'].append({self.name: ctr.properties})
        return info

    def check_controller_state(self):
        for ctr in self.unavailable:
            if not ctr.get_property('DRIVER'):
                raise NonDriverAttached('\nTest *FAIL* since following Controller was not claimed '
                                        'any driver, please manually attach driver or ask help from '
                                        'OpenAnolis community:\n    %s' % ctr.name)


class FakeComponent(Component):
    def __init__(self, logdir, cases, category, args):
        super(FakeComponent, self).__init__(logdir, cases, category, args)
        self.fake_device_name = ''

    def build_works(self):
        self.tested_controller = [self.available[self.test_device_index]]
        ctr = self.tested_controller[0]
        for gname, cases_inst in self.groups:
            w_inst = []
            for case in cases_inst:
                test = Test(case, ctr.available_devices[0], ctr, self.logdir, self.mode)
                w_inst.append(select_worker(test))
            self.workers.append([gname, w_inst])

    def build(self, dstree):
        dev = FakeDevice(self.fake_device_name)
        ctr = Controller(dev, self)
        ctr.devices.append(dev)
        ctr.available_devices.append(dev)
        self.available.append(ctr)
        self.build_works()


class Audio(Component):
    CLASS_CODE = ['000401', '000403']

    def __init__(self, logdir, cases, category, args):
        super(Audio, self).__init__(logdir, cases, category, args)


class Video(Component):
    CLASS_CODE = ['000300', '000301', '000380']

    def __init__(self, logdir, cases, category, args):
        super(Video, self).__init__(logdir, cases, category, args)
        # self.name = 'video'
        self.build_cases(cases)

    def build(self, dstree):
        for dev in dstree.devices:
            if dev.classcode and is_sub_class_code(self.__class__.CLASS_CODE, dev.classcode) \
                                                                        and dev.is_pci_endpoint():
                ctr = Controller(dev, self)
                self.mapping(ctr, dstree)
                if ctr.valid:
                    self.available.append(ctr)
                else:
                    self.unavailable.append(ctr)
        self.sort()
        self.display('video device')
        if not self.available:
            raise NonAvaliableController('No available video device to test! Please double check '
                                         'graphics driver whether attached successful.')
        self.build_works()

    def mapping(self, ctr, dstree):
        if ctr.get_property('DRIVER'):
            ctr.devices.append(ctr.device_inst)
            ctr.available_devices.append(ctr.device_inst)
            ctr.valid = True

    def build_works(self):
        if self.test_device_index + 1 > len(self.available):
            raise NonAvaliableController('--index %s is not right, current only have %d video device'
                                         % (self.test_device_index + 1, len(self.available)))
        self.tested_controller = [self.available[self.test_device_index]]
        for tested in self.tested_controller:
            for gname, cases_inst in self.groups:
                w_inst = []
                for case in cases_inst:
                    for device in tested.devices:
                        test = Test(case, device, tested, self.logdir, self.mode)
                        w_inst.append(select_worker(test))
                self.workers.append([gname, w_inst])


class GPU(Video):
    # 000302 NVIDIA
    # 000B40 denglin
    # 000380 Chengdu Haiguang
    # 00120000 Iluvatar CoreX
    CLASS_CODE = ['000302', '00120000', '000B40', '000380']

    def __init__(self, logdir, cases, category, args):
        super(Video, self).__init__(logdir, cases, category, args)
        self.build_cases(cases)


class CPU(Component):
    def __init__(self, logdir, cases, category, args):
        super(CPU, self).__init__(logdir, cases, category, args)
        _, self.cpuinfo = run_local_cmd('lscpu', exit_msg='Failed to get cpu info.')
        info = dump_dmidecode('dmidecode -t processor', 'processor', 'Processor Information\n\t')
        if not info or 'Version' not in info[0]:
            raise NonAvaliableDevice('Failed to get cpu model name!')
        self.model_name = info[0]['Version']
        self.build_cases(cases)

    def build(self, dstree):
        for dev in dstree.devices:
            if dev.get_property('SUBSYSTEM') == 'cpu' and 'cpu0' in dev.get_property('SYSPATH'):
                ctr = Controller(dev, self)
                dev.properties.update({'MODEL_NAME': self.model_name})
                ctr.devices.append(dev)
                ctr.available_devices.append(dev)
                self.available.append(ctr)
                ctr.device_inst.name = self.model_name
                break
        if self.output:
            print('Available: %s' % self.model_name)
        logging.info('Available: %s' % self.model_name)
        logging.info('CPU info:\n%s' % self.cpuinfo)
        if not self.available:
            raise NonAvaliableController('No available cpu to test!')
        self.build_works()

    def build_works(self):
        self.tested_controller = [self.available[self.test_device_index]]
        ctr = self.tested_controller[0]
        for gname, cases_inst in self.groups:
            w_inst = []
            for case in cases_inst:
                test = Test(case, ctr.available_devices[0], ctr, self.logdir, self.mode)
                w_inst.append(select_worker(test))
            self.workers.append([gname, w_inst])


class Memory(FakeComponent):
    def __init__(self, logdir, cases, category, args):
        super(Memory, self).__init__(logdir, cases, category, args)
        self.build_cases(cases)
        self.physical_memory = get_memory_info()
        self.fake_device_name = self.physical_memory[0][0]


class BIOS(FakeComponent):
    def __init__(self, logdir, cases, category, args):
        super(BIOS, self).__init__(logdir, cases, category, args)
        self.build_cases(cases)
        self.fake_device_name = get_bios_vendor()


class Network(Component):
    CLASS_CODE = ['0002']

    def __init__(self, logdir, cases, category, args):
        super(Network, self).__init__(logdir, cases, category, args)
        self.build_cases(cases)
        if args.lts_ip:
            self.env.update({'LTS_IPADDRESS': args.lts_ip})

    def build(self, dstree):
        cur_pci_bus_device = []
        for dev in dstree.devices:
            if dev.syspath.rsplit('.', 1)[0] in cur_pci_bus_device:
                continue
            if dev.classcode and is_sub_class_code(Network.CLASS_CODE, dev.classcode) \
                    and dev.is_pci_endpoint():
                ctr = Controller(dev, self)
                cur_pci_bus_device.append(ctr.bus_device)
                self.mapping(ctr, dstree)
                if ctr.valid:
                    self.available.append(ctr)
                else:
                    self.unavailable.append(ctr)
        self.sort()
        self.display('network controller')
        if not self.available:
            raise NonAvaliableController('No available network controller to test! Please config at least '
                                         'a port with dhcp IP address for unavailable network controllers.')
        self.build_works()
        self.check_controller_state()

    def mapping(self, ctr, dstree):
        for dev in dstree.devices:
            if dev.syspath.startswith(ctr.bus_device):
                if dev is ctr.device_inst:
                    continue
                if 'net' in dev.get_property('SUBSYSTEM'):
                    if self.is_single_mode:
                        if not dev.get_property('INTERFACE'):
                            continue
                    else:
                        if not is_network_interface_linkup(dev.get_property('INTERFACE')):
                            continue
                    ctr.devices.append(dev)
                    if ctr.get_property('DRIVER'):
                        ctr.valid = True
                    ctr.available_devices.append(dev)
        logging.debug('network ctroller %s valid value is %s, ports are %s'
                      % (ctr.devinfo, ctr.valid, ','.join([dev.get_property('INTERFACE').strip()
                      for dev in ctr.available_devices])))

    def build_works(self):
        if self.test_device_index + 1 > len(self.available):
            raise NonAvaliableController('--index %s is not right, current only have %d network controller'
                                         % (self.test_device_index + 1, len(self.available)))
        self.tested_controller = [self.available[self.test_device_index]]
        for tested in self.tested_controller:
            for gname, cases_inst in self.groups:
                w_inst = []
                for case in cases_inst:
                    if case.required:
                        if tested.test_mode not in case.required:
                            continue
                    tested.device_inst.properties.update(self.env)
                    interfaces = [device.get_property('INTERFACE') for device in tested.devices]
                    tested.device_inst.properties.update(
                        {'INTERFACES': ' '.join(interfaces)})
                    test = Test(case, tested.device_inst, tested, self.logdir, self.mode)
                    w_inst.append(select_worker(test))
                if w_inst:
                    self.workers.append([gname, w_inst])


class Storage(Component):
    CLASS_CODE = ['000100', '000101', '000104', '000105',
                  '000106', '000107', '000180']

    def __init__(self, logdir, cases, category, args):
        super(Storage, self).__init__(logdir, cases, category, args)
        self.build_cases(cases)
        self.partition2drive = map_partition_to_drive()
        self.all_disks = get_all_disks()
        self.boot_disk = get_boot_disk(self.partition2drive)
        self.pvs_disk = get_pvs_disk(self.partition2drive)
        logging.debug('boot disk is %s' % self.boot_disk)
        logging.debug('pvs disk is %s' % self.pvs_disk)

    def build(self, dstree):
        #TODO need to fix from controller level
        cur_pci_bus_device = []
        for dev in dstree.devices:
            if dev.syspath.rsplit('.', 1)[0] in cur_pci_bus_device:
                continue
            if dev.classcode and is_sub_class_code(self.__class__.CLASS_CODE, dev.classcode) \
                    and dev.is_pci_endpoint():
                ctr = StorageController(dev, self)
                cur_pci_bus_device.append(ctr.bus_device)
                self.mapping(ctr, dstree)
                # sometimes disk status is UGood for RAID controller, so ancert cannot detect
                # any disk from controller. The test will failed with no available devices.
                if self.__class__.__name__.lower() in ['raid']:
                    ctr.valid = True
                    fdev = FakeDevice('fake device')
                    fdev.partition_inst = []
                    fdev.dev_path = ''
                    ctr.available_devices.append(fdev)
                if ctr.valid:
                    self.available.append(ctr)
                else:
                    self.unavailable.append(ctr)
        if not self.available:
            logging.warning('No available controller for storage.')
        else:
            for key, val in self.all_disks.items():
                for partition in val[2]:
                    partition.properties.update({'DRIVE_FULL_NAME': val[1].get_property('DRIVE_FULL_NAME')})
        for _ in range(len(self.available)):
            if self.__class__.__name__.lower() in ['raid']:
                break
            ctr = self.available.pop(0)
            for _ in range(len(ctr.available_devices)):
                dk = ctr.available_devices.pop(0)
                if dk.is_partition and not dk.partition_inst:
                   continue
                ctr.available_devices.append(dk)
            if ctr.available_devices:
                self.available.append(ctr)
            else:
                logging.warning('storage ctr %s dose not have any available disk or partition' % ctr.name)
                self.unavailable.append(ctr)
        for ctr in self.available:
            if self.__class__.__name__.lower() in ['raid']:
                break
            ctr.available_devices = sorted(ctr.available_devices, key=lambda d: len(d.partition_inst))
            for dk in ctr.available_devices:
                dk.filter_partition()
                if dk.partition_inst:
                    for pt in dk.partition_inst:
                        if pt.mount_point:
                            pt.properties.update({'ANCERT_TEST_MOUNT_POINT': pt.mount_point})
                            logging.debug('disk %s is a partition, select mount point %s for test'
                                          % (pt.dev_path, pt.mount_point))
                else:
                    if dk.is_boot_disk and dk.mount_point and dk.mount_point_free_space >= 10:
                        dk.properties.update({'ANCERT_TEST_MOUNT_POINT': dk.mount_point})
                        logging.debug('disk %s is a boot disk, select mount point / for test' % dk.dev_path)
                    else:
                        dk.properties.update({'IS_RAW_DISK': 'true'})
                        logging.debug('consider disk %s as raw disk' % dk.dev_path)
        self.sort()
        self.display('storage controller')
        if not self.available:
            raise NonAvaliableController('No available {0} controller to test! Please config at least '
                                         'a free disk(>10GB) for above unavailable {0} controllers.'.format(self.name))
        self.build_works()
        self.check_controller_state()

    def mapping(self, ctr, dstree):
        for dev in dstree.devices:
            if not dev.syspath.startswith(ctr.bus_device):
                continue
            if dev.get_property('DEVTYPE') not in ['partition', 'disk'] or not dev.get_property('DEVTYPE'):
                continue
            if dev.get_property('SUBSYSTEM') not in ['block']:
                continue
            #TODO support cdrom test in future
            if dev.get_property('ID_CDROM') == '1':
                logging.info('%s is a cdrom' % dev.get_property('SYSPATH'))
                continue
            dk = DevDisk(dev, ctr, self)
            if dk.dev_path in self.all_disks:
                self.all_disks[dk.dev_path][0] = ctr
                self.all_disks[dk.dev_path][1] = dk
                dk.partition_inst = self.all_disks[dk.dev_path][2]
            else:
                for key, val in self.all_disks.items():
                    if dk.dev_path.startswith(key) and key != dk.dev_path and dk.mount_point:
                        val[2].append(dk)
                        break
                else:
                    logging.warning('failed to detect the disk for %s' % dk.dev_path)
            disk_size_gb = get_disk_size(dk.get_property('DEVNAME'))
            if disk_size_gb < 10:
                logging.warning('%s size is %sG, skip this disk' % (dk.dev_path, disk_size_gb))
                continue
            dk.properties.update({'DISK_SIZE': '%sG' % disk_size_gb})
            dk.properties.update({'DEVICE_NAME': ctr.name})
            if not dk.is_partition:
                drive_name = ''
                if dk.get_property('ID_VENDOR'):
                    drive_name += dk.get_property('ID_VENDOR') + ' '
                if dk.get_property('ID_MODEL') in ['Virtual_disk']: # raid
                    drive_name += dk.get_property('ID_MODEL') + ' '
                if dk.get_property('ID_SERIAL'):
                    drive_name += dk.get_property('ID_SERIAL').replace('_', ' ')
                if not drive_name.strip() and self.is_virtual_platform:
                    drive_name = 'Virtual Disk' # qemu
                dk.properties.update({'DRIVE_FULL_NAME': drive_name})
            if not dk.is_partition:
                ctr.devices.append(dk)
            if dev is ctr.device_inst:
                continue
            if dk.is_partition or (dk.is_pvs_disk and not dk.is_boot_disk):
                logging.debug('disk %s is partition or pvs disk' % dk.dev_path)
                continue
            if dk.is_boot_disk and not self.is_system_test:
                logging.debug('disk %s is boot disk' % dk.dev_path)
                continue
            ctr.available_devices.append(dk)
        ctr.set_valid(self)
        logging.info('storage controller %s valid value is %s' % (ctr.devinfo, ctr.valid))


    def build_works(self):
        if self.test_device_index + 1 > len(self.available):
            raise NonAvaliableController('--index %s is not right, current only have %d storage controller'
                                         % (self.test_device_index + 1, len(self.available)))
        if self.is_system_test:
            self.tested_controller = self.available
        else:
            self.tested_controller = [self.available[self.test_device_index]]
        for tested in self.tested_controller:
            for gname, cases_inst in self.groups:
                w_inst = []
                for case in cases_inst:
                    device = tested.pickup_device()
                    if case.required:
                        if not set(case.required).issubset(device.attributes):
                            logging.warning('skip case %s, since case require %s, but device %s only has %s'
                                          % (case.name, case.required, device.dev_path, device.attributes))
                            continue
                    test = Test(case, device, tested, self.logdir, self.mode)
                    w_inst.append(select_worker(test))
                if w_inst:
                    self.workers.append([gname, w_inst])


class NVMe(Storage):
    CLASS_CODE = ['000108']

    def __init__(self, logdir, cases, category, args):
        super(NVMe, self).__init__(logdir, cases, category, args)


class FC(Storage):
    CLASS_CODE = ['000C04']

    def __init__(self, logdir, cases, category, args):
        super(FC, self).__init__(logdir, cases, category, args)


class RAID(Storage):
    CLASS_CODE = ['000104']

    def __init__(self, logdir, cases, category, args):
        super(RAID, self).__init__(logdir, cases, category, args)


class Kdump(FakeComponent):
    def __init__(self, logdir, cases, category, args):
        super(Kdump, self).__init__(logdir, cases, category, args)
        self.build_cases(cases)


class Disk(FakeComponent):
    def __init__(self, logdir, cases, category, args):
        super(Disk, self).__init__(logdir, cases, category, args)
        self.build_cases(cases)


class Suspend(FakeComponent):
    def __init__(self, logdir, cases, category, args):
        super(Suspend, self).__init__(logdir, cases, category, args)
        self.build_cases(cases)


class Misc(FakeComponent):
    def __init__(self, logdir, cases, category, args):
        super(Misc, self).__init__(logdir, cases, category, args)
        self.build_cases(cases)


class IPMI(FakeComponent):
    def __init__(self, logdir, cases, category, args):
        super(IPMI, self).__init__(logdir, cases, category, args)
        self.fake_device_name = get_bmc_vendor(self)
        self.build_cases(cases)

    def build(self, dstree):
        dev = FakeDevice(self.fake_device_name)
        dev.properties.update({'DRIVER': 'ipmi_si'})
        dev.driver = 'ipmi_si'
        ctr = Controller(dev, self)
        ctr.devices.append(dev)
        ctr.available_devices.append(dev)
        self.available.append(ctr)
        self.build_works()
